﻿using System;
using System.Collections.Generic;
using System.Linq;
using Contact.DAL.Models.Worktime;
using EFDataModel;

namespace TimeManagement
{
    public class TimeManagement
    {
        #region Static Functions
        public static TimeManagement Management(EFWorktime entry, EFModelContainer efModelContainer)
        {
            return new TimeManagement(entry, efModelContainer);
        }

        /// <summary>
        /// Berechnen der Monatlichen Maximalen Arbeitsstunden, für das übergebene Datum.
        /// TODO später evtl. mal live berechnen, wenn man z.B. die Arbeitszeit mitten im Monat ändert von 30h auf 40h
        /// dann wird die Gesamtzeitanzeige bei der aktuellen Berechnung nicht stimmen.
        /// </summary>
        /// <param name="setting">Arbeitszeitsettings mit den Einstellungen wann jmd arbeiten muss.</param>
        /// <param name="date">Aus dem Datum wird der Monat extrahiert für den die Zeit berechnet werden soll</param>
        public static int GetMonthlyWorktimeInMinutes(EFWorktimeSetting setting, DateTime date, List<EFWorktime> worktimes)
        {
            int dailayWorktime = TimeFunctions.DailyWorktimeInMinutes(setting.EFAuthentication.EFWorktimeSetting);
            int worktimeSummary = 0;
            //Den ersten des Monats ermitteln
            DateTime currentMonth = new DateTime(date.Year, date.Month, 1);
            //Jeden Tag des Monats durchgehen
            while (currentMonth.Month == date.Month)
            {
                //Prüfen ob der Tag zu einem Arbeitstag gehört der in den Settings hinterlegt wurde.
                if ((currentMonth.DayOfWeek == DayOfWeek.Saturday && setting.WorkingDays.Sat) ||
                    (currentMonth.DayOfWeek == DayOfWeek.Sunday && setting.WorkingDays.Sun) ||
                    (currentMonth.DayOfWeek == DayOfWeek.Monday && setting.WorkingDays.Mon) ||
                    (currentMonth.DayOfWeek == DayOfWeek.Tuesday && setting.WorkingDays.Tue) ||
                    (currentMonth.DayOfWeek == DayOfWeek.Wednesday && setting.WorkingDays.Wed) ||
                    (currentMonth.DayOfWeek == DayOfWeek.Thursday && setting.WorkingDays.Thu) ||
                    (currentMonth.DayOfWeek == DayOfWeek.Friday && setting.WorkingDays.Fri))
                {
                    //Wenn es sich um einen Tag handelt an dem gearbeitet werden soll prüfen ob es sich hier evtl. um einen
                    //Feiertag handelt, dieser darf nicht mit in die Arbeitszeit einbezogen werden.
                    if (worktimes.Any(p => p.EntryDate == currentMonth && p.IsLegalHoliday))
                    {
                        currentMonth = currentMonth.AddDays(1);
                        continue;
                    }

                    worktimeSummary += dailayWorktime;
                }

                currentMonth = currentMonth.AddDays(1);
            }

            return worktimeSummary;
        }
        #endregion

        #region Member
        /// <summary>
        /// Die Zeitabstände mit denen gerechnet wird, da wir eh nur im 5 Minuten takt Zeiten angeben können, dann 
        /// kann dieser Zeitgeber hier direkt verwendet werden
        /// </summary>
        public static int StepSize = 5;

        /// <summary>
        /// Der Aktuelle Arbeitstageintrag.
        /// </summary>
        private EFWorktime Worktime { get; set; }

        /// <summary>
        /// DB Container
        /// </summary>
        private EFModelContainer EFContainer { get; set; }

        /// <summary>
        /// Die Liste mit den Arbeitszeiteinträgen für das EntryDate und dem passenden User.
        /// </summary>
        private List<EFWorktimeEntry> WorktimeEntries { get; set; }

        private WorktimeRepository WorktimeRepository { get; set; }
        #endregion

        #region Konstruktor
        public TimeManagement(EFWorktime entry, EFModelContainer efModelContainer)
        {
            Worktime = entry;
            EFContainer = efModelContainer;
            WorktimeRepository = new WorktimeRepository(EFContainer);
            //Die Liste nur einmal aus der DB abrufen.
            WorktimeEntries = WorktimeRepository.GetWorktimeEntriesByDay(Worktime.EFAuthenticationID, Worktime.EntryDate).ToList();
        }
        #endregion

        #region Public Functions
        /// <summary>
        /// Berechnet die Zeiteinträge für den WorktimeType für das aktuelle WorktimeObjekt.
        /// </summary>
        /// <param name="worktimeType">Der WorktimeType der erneut berechnet werden soll</param>
        public void RecalculateTimeEntries(EWorktimeType worktimeType)
        {
            //Die tägliche Arbeitszeit ermitteln und speichern, nur überschreiben wenn noch kein Wert hinterlegt wurde.
            //Damit wird sichergestellt, das die Arbeitszeit nicht angepasst wird wenn diese sich evtl. schon geändert hat.
            if (Worktime.DailyWorktime == 0)
            {
                Worktime.DailyWorktime = TimeFunctions.DailyWorktimeInMinutes(Worktime.EFAuthentication.EFWorktimeSetting);
            }

            double time = 0;
            double breaktime = 0;

            //Je Nach Arbeitszeittyp die passende Arbeitszeit ermitteln
            switch (worktimeType)
            {
                case EWorktimeType.Worktime:
                    GetTimeWithoutBreaktime(out breaktime, out time, EWorktimeType.Worktime);
                    Worktime.Worktime = (int)time;
                    break;

                case EWorktimeType.Breaktime:
                    //Wenn eine Pause eingetragen wird, müssen auch alle anderen Berechnungen die von der Pause abhängig sind noch einmal
                    //überprüft und angepasst werden.
                    GetTimeWithoutBreaktime(out breaktime, out time, EWorktimeType.Worktime);
                    Worktime.Worktime = (int)time;
                    Worktime.Breaktime = (int)breaktime;
                    //Die Funktion noch einmal aufrufen um die jeweiligen Einträge erneut zu berechnen.
                    RecalculateTimeEntries(EWorktimeType.Qualification);
                    RecalculateTimeEntries(EWorktimeType.Projecttime);
                    break;

                case EWorktimeType.Illness:
                    Worktime.Illness = (int)GetEntrytimeByWorktimeType(worktimeType);
                    break;

                case EWorktimeType.Qualification:
                    GetTimeWithoutBreaktime(out breaktime, out time, EWorktimeType.Qualification);
                    Worktime.Qualificationtime = (int)time;
                    break;

                case EWorktimeType.Holiday:
                    Worktime.Holidaytime = (int)GetEntrytimeByWorktimeType(worktimeType);
                    //Bei Urlaubstagen aufpassen, diese können nie länger wie die Gesamtarbeitszeit eines Tages sein!.
                    if (Worktime.Holidaytime > Worktime.DailyWorktime)
                    {
                        Worktime.Holidaytime = Worktime.DailyWorktime;
                    }

                    break;

                case EWorktimeType.Projecttime:
                    GetTimeWithoutBreaktime(out breaktime, out time, EWorktimeType.Projecttime);
                    Worktime.Projecttime = (int)time;
                    break;
            }

            //Die Überstunden werden jedesmal komplett neu für den aktuellen Arbeitstag berechnet!
            Worktime.Overtime = GetOvertime();
        }

        /// <summary>
        /// Setzt die passende Arbeitszeit für das aktuelle Worktime Objekt.
        /// </summary>
        /// <returns>TRUE->Zeit konnte erfolgreich gesetzt werden | FALSE->Es ist ein Fehler beim Berechnen der Zeit aufgetreten.</returns>
        public void SetTimesForWorktime(EFWorktimeEntry entry)
        {
            RecalculateTimeEntries(entry.WorkingtimeType);
        }
        #endregion

        #region Private Functions
        /// <summary>
        /// Ermittelt die Arbeitszeit für den aktuellen Tag, anhand des gesetzten EFWorktime
        /// </summary>
        /// <returns>Errechnet anhand der ganzen WorktimeEntries für den aktuellen Tag die Anzahl der Arbeitsstunden</returns>
        private double GetEntrytimeByWorktimeType(EWorktimeType worktimeType)
        {
            //nur für die Krankheit und die Ferien werden die Arbeitszeiten hier berechnet.
            if (worktimeType == EWorktimeType.Illness || worktimeType == EWorktimeType.Holiday)
            {
                //Zwei Listen eine mit der Arbeitszeit und eine mit der Pausenzeit hier werden direkt die Absoluten Minuten gespeichert
                //und anhand dieser wird berechnet wann eine Pause eine Arbeitszeit überlappt.
                List<double> entryList = new List<double>();

                //Wichtig, ist die Reihenfolge, es muss immer nach dem Startdatum sortiert sein, damit die Berechnungen richtig funktionieren
                foreach (EFWorktimeEntry entry in WorktimeEntries.OrderBy(p => p.StartTime))
                {
                    //Die Arbeitszeit für einen Tag wird anhand der Arbeitzeit minus der Pause berechnet
                    if (entry.WorkingtimeType == worktimeType)
                    {

                        TimeSpan timeStart = new TimeSpan(0, entry.StartTime.Hour, entry.StartTime.Minute, 0);
                        TimeSpan timeEnd = new TimeSpan(0, entry.EndTime.Hour, entry.EndTime.Minute, 0);

                        //Die Min und Max Zeiten ermitteln, damit wir alle Absoluten Minuten durchgehen können.
                        double minTime = timeStart.TotalMinutes;
                        double maxTime = timeEnd.TotalMinutes;

                        //Alle Minuten durchgehen und der jeweils der Liste hinzufügen
                        for (double i = minTime; i < maxTime; i = i + StepSize)
                        {
                            //Wenn der Eintrag noch nicht in der Liste ist, hinzufügen.
                            if (!entryList.Contains(i))
                            {
                                entryList.Add(i);
                            }
                        }
                    }
                }

                return entryList.Count * StepSize;
            }

            //Alle anderen Einträge werden anders berechnet
            double time = 0, breaktime = 0;
            GetTimeWithoutBreaktime(out breaktime, out time, worktimeType);
            return time;
        }

        /// <summary>
        /// Ermittelt die Arbeitszeit für den aktuellen Tag, anhand des gesetzten EFWorktime
        /// </summary>
        /// <param name="breaktime">Gibt die Pausenzeit zurück</param>
        /// <param name="worktime">Gibt die Arbeitszeit abzüglich der Pause zurück </param>
        /// <param name="worktimeType">Der Arbeitszeittyp für den die Pause herausgerechnet werden soll </param>
        /// <returns>Errechnet anhand der ganzen WorktimeEntries für den aktuellen Tag die Anzahl der Arbeitsstunden</returns>
        private void GetTimeWithoutBreaktime(out double breaktime, out double worktime, EWorktimeType worktimeType)
        {
            worktime = 0;
            breaktime = 0;

            //Zwei Listen eine mit der Arbeitszeit und eine mit der Pausenzeit hier werden direkt die Absoluten Minuten gespeichert
            //und anhand dieser wird berechnet wann eine Pause eine Arbeitszeit überlappt.
            List<double> worktimeTypeList = new List<double>();
            List<double> breaktimeList = new List<double>();

            //Wichtig, ist die Reihenfolge, es muss immer nach dem Startdatum sortiert sein, damit die Berechnungen richtig funktionieren
            foreach (EFWorktimeEntry entry in WorktimeEntries.OrderBy(p => p.StartTime))
            {
                //Die Arbeitszeit für einen Tag wird anhand der Arbeitzeit minus der Pause berechnet
                if (entry.WorkingtimeType == worktimeType || entry.WorkingtimeType == EWorktimeType.Breaktime)
                {
                    TimeSpan timeStart = new TimeSpan(0, entry.StartTime.Hour, entry.StartTime.Minute, 0);
                    TimeSpan timeEnd = new TimeSpan(0, entry.EndTime.Hour, entry.EndTime.Minute, 0);

                    //Die Min und Max Zeiten ermitteln, damit wir alle Absoluten Minuten durchgehen können.
                    double minTime = timeStart.TotalMinutes;
                    double maxTime = timeEnd.TotalMinutes;

                    //Alle Minuten durchgehen und der jeweils passenden Liste die jeweilige Minute zuweisen.
                    for (double i = minTime; i < maxTime; i = i + StepSize)
                    {
                        if (entry.WorkingtimeType == EWorktimeType.Breaktime)
                        {
                            if (!breaktimeList.Contains(i))
                            {
                                breaktimeList.Add(i);
                            }
                        }
                        else
                        {
                            //Die Projektzeiten werden immer komplett gerechnet, ohne Werte "abzuschneiden" daher wird hier jeder 
                            //Wert der Liste hinzugefügt.
                            if (!worktimeTypeList.Contains(i) || worktimeType == EWorktimeType.Projecttime)
                            {
                                worktimeTypeList.Add(i);
                            }
                        }
                    }
                }
            }

            //die Pausenzeiten die auch in der Arbeitszeitliste enthalten sind, aus der Arbeitszeitliste entfernen.
            foreach (double breakTimeEntry in breaktimeList)
            {
                if (worktimeTypeList.Contains(breakTimeEntry))
                {
                    //Alle Einträge entfernen und nicht nur den ersten! (Wichtig für die Projektzeiten, da hier mehrere Pro Zeitstempel existieren können)
                    bool remove = worktimeTypeList.Remove(breakTimeEntry);
                    while (remove)
                    {
                        //Berechnen der Pausenzeit anhand der Stepsize und nur für Einträge die sich auch mit der Arbeitszeit überlappen
                        remove = worktimeTypeList.Remove(breakTimeEntry);
                        breaktime = breaktime + StepSize;
                    }
                }
            }

            //Berechnen der Arbeitszeit anhand der Einträge in der Arbeitszeitliste und der Stepsize.
            worktime = worktimeTypeList.Count * StepSize;
        }

        /// <summary>
        /// Ermittelt die Überstunden für den aktuellen Tag über alle wichtigen Einträge für diesen Tag.
        /// </summary>
        private int GetOvertime()
        {
            int overtime = 0;

            //Zwei Listen eine mit der Arbeitszeit und eine mit der Pausenzeit hier werden direkt die Absoluten Minuten gespeichert
            //und anhand dieser wird berechnet wann eine Pause eine Arbeitszeit überlappt.
            List<double> worktimeList = new List<double>();
            List<double> breaktimeList = new List<double>();
            List<double> illnessList = new List<double>();
            List<double> holidayList = new List<double>();

            //Wichtig, ist die Reihenfolge, es muss immer nach dem Startdatum sortiert sein, damit die Berechnungen richtig funktionieren
            foreach (EFWorktimeEntry entry in WorktimeEntries.OrderBy(p => p.StartTime))
            {
                //Die Arbeitszeit für einen Tag wird anhand der Arbeitzeit minus der Pause berechnet
                if (entry.WorkingtimeType == EWorktimeType.Worktime || entry.WorkingtimeType == EWorktimeType.Breaktime ||
                    entry.WorkingtimeType == EWorktimeType.Illness || entry.WorkingtimeType == EWorktimeType.Holiday)
                {
                    TimeSpan timeStart = new TimeSpan(0, entry.StartTime.Hour, entry.StartTime.Minute, 0);
                    TimeSpan timeEnd = new TimeSpan(0, entry.EndTime.Hour, entry.EndTime.Minute, 0);

                    //Die Min und Max Zeiten ermitteln, damit wir alle Absoluten Minuten durchgehen können.
                    double minTime = timeStart.TotalMinutes;
                    double maxTime = timeEnd.TotalMinutes;

                    //Alle Minuten durchgehen und der jeweils passenden Liste die jeweilige Minute zuweisen.
                    for (double i = minTime; i < maxTime; i = i + StepSize)
                    {
                        //Wenn es sich um eine Pausenzeit handelt und diese noch nicht in der Pausenzeitliste enthalten ist
                        //dann diese der Liste hinzufügen.
                        if (entry.WorkingtimeType == EWorktimeType.Breaktime && !breaktimeList.Contains(i))
                        {
                            breaktimeList.Add(i);
                        }

                        if (entry.WorkingtimeType == EWorktimeType.Worktime && !worktimeList.Contains(i))
                        {
                            worktimeList.Add(i);
                        }

                        if (entry.WorkingtimeType == EWorktimeType.Illness && !illnessList.Contains(i))
                        {
                            illnessList.Add(i);
                        }

                        if (entry.WorkingtimeType == EWorktimeType.Holiday && !holidayList.Contains(i))
                        {
                            holidayList.Add(i);
                        }
                    }
                }
            }

            //die Pausenzeiten kann man aus der Arbeitszeitliste entfernt werden. Da wir diese hier entfernen
            //kann auch keine Überschneidung mehr mit anderen Einträgen wie Krankheite / Pause und Arbeitszeit passieren.
            //Für Krankheit und Urlaub sind Pausenzeiten nicht relevant.
            foreach (double breakTimeEntry in breaktimeList)
            {
                if (worktimeList.Contains(breakTimeEntry))
                {
                    worktimeList.Remove(breakTimeEntry);
                }
            }

            //Die Zeiten in denen Sich die Urlaubs und die Krankheitsliste überschneiden, muss auch aus einer der beiden Listen
            //entfernt werden um die Gesamtzeit zu ermitteln für einen Tag die nicht als Arbeitszeit angerechnet werden kann.
            //Denn Urlaub und Krankheit zählen quasi "gleich" und werden vom täglichen Arbeitzeitpensum abgezogen
            //Wir entfernen die Krankheitsminuten aus der Urlaubsliste, dann erhalten wir eine Zeitlinie
            foreach (double holidayEntry in holidayList)
            {
                if (illnessList.Contains(holidayEntry))
                {
                    illnessList.Remove(holidayEntry);
                }
            }

            //Prüfen ob es sich um einen Offiziellen Feiertag handelt, hier zählt jede Stunde als Überstunde die geleistet wurde und die Restlichen Einträge sind "egal"
            //Hier wird nur beachtet das auch die Pausenzeit entsprechend abgezogen wurde. Auch wenn es sich bei dem aktuellen Tag nicht um einen offiziellen Arbeitstag handelt
            //wird nur die Arbeitszeit als Überstunden berechnet.
            if (Worktime.IsLegalHoliday || !((EFWorkingDays)Worktime.WorktimeDays).HasFlag(TimeFunctions.GetWorkingDay(Worktime.EntryDate)))
            {
                return worktimeList.Count * StepSize;
            }

            //Jetzt bleiben nur noch die Ausgeführte Arbeitszeit übrig, hier ist es egal ob sich diese mit der Urlaubszeit bzw. Krankheitszeit
            //überschneided, da hier nur die Gesamtstunden für einen Tag zählen. Daher errechnen wir erst die geleisteten Arbeitsstunden
            //und dann die Krankheit und Urlaubszeit und berechnen dann anhand der Täglichen Arbeitszeit die geleisteten Überstunden.
            int freeTime = holidayList.Count * StepSize + illnessList.Count * StepSize;
            int worktime = worktimeList.Count * StepSize;
            overtime = freeTime + worktime - Worktime.DailyWorktime;

            //Berechnen der Arbeitszeit anhand der Einträge in der Arbeitszeitliste und der Stepsize.
            return overtime;
        }
        #endregion
    }
}
